package com.xmall.search.service.impl.other;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.PageUtil;
import cn.hutool.core.util.StrUtil;
import co.elastic.clients.elasticsearch._types.FieldValue;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.aggregations.Aggregate;
import co.elastic.clients.elasticsearch._types.mapping.*;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.json.JsonData;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xmall.define.base.Result;
import com.xmall.define.dict.DataTypeEnum;
import com.xmall.define.dict.SortEnum;
import com.xmall.define.dto.es.EsSkuDto;
import com.xmall.define.dto.product.EsBaseAttrDto;
import com.xmall.define.request.es.EsSpuSearchRequest;
import com.xmall.msg.entity.es.ProductUpdateMsg;
import com.xmall.search.define.constants.SearchProperties;
import com.xmall.search.service.IProductDeleteRetryLogService;
import com.xmall.search.service.IProductUpdateRetryLogService;
import com.xmall.search.service.other.IElasticSearchService;
import com.xmall.search.service.other.IProductService;
import com.xmall.search.service.remote.ProductServiceFeign;
import com.xmall.utils.XmallUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

@Slf4j
@Service
public class ProductServiceImpl implements IProductService {

    @Resource
    private SearchProperties searchProperties;

    @Resource
    private ProductServiceFeign productServiceFeign;

    @Resource
    private IProductUpdateRetryLogService productUpdateRetryLogService;

    @Resource
    private IProductDeleteRetryLogService productDeleteRetryLogService;

    @Resource
    private IElasticSearchService elasticSearchService;

    /**
     * 更新索引映射
     */
    @Override
    public void updateMapping() {
        elasticSearchService.updateMapping(searchProperties.getProductIndexName(), builder ->
            builder.properties("id", np -> np.long_(new LongNumberProperty.Builder().build()))
                .properties("esVersion", p -> p.integer(new IntegerNumberProperty.Builder().build()))
                .properties("categoryId", p -> p.long_(new LongNumberProperty.Builder().build()))
                .properties("spuId", np -> np.long_(new LongNumberProperty.Builder().index(false).build()))
                .properties("title", np -> np.text(new TextProperty.Builder().analyzer("ik_smart").build()))
                .properties("price", np -> np.double_(new DoubleNumberProperty.Builder().build()))
                .properties("image", np -> np.keyword(new KeywordProperty.Builder().index(false).build()))
                .properties("saleNum", np -> np.integer(new IntegerNumberProperty.Builder().build()))
                .properties("stockNum", np -> np.integer(new IntegerNumberProperty.Builder().build()))
                .properties("lockedNum", np -> np.integer(new IntegerNumberProperty.Builder().build()))
                .properties("hasStock", np -> np.boolean_(new BooleanProperty.Builder().build()))
                .properties("spuAttrList", p -> p.nested(n -> n
                    .properties("attrId", np -> np.long_(new LongNumberProperty.Builder().build()))
                    .properties("attrValue", np -> np.keyword(new KeywordProperty.Builder().build()))
                ))
        );
    }

    /**
     * 更新商品
     */
    @Transactional
    @Override
    public void update(ProductUpdateMsg msg) {
        elasticSearchService.delete(searchProperties.getProductIndexName(), builder ->
            builder.query(q -> q.match(m -> m.field("spuId").query(msg.getSpuId())))
        );

        Result<List<EsSkuDto>> remoteResult = productServiceFeign.buildEsSku(msg.getSpuId(), msg.getEsVersion());
        XmallUtils.checkRemoteResult(remoteResult);

        List<EsSkuDto> skuList = remoteResult.getData();
        if(CollectionUtil.isEmpty(skuList)) {
            log.error("EsService updateProduct: {}: update esVersion error", msg.getSpuId());
            return;
        }

        List<Long> successIdList = elasticSearchService.save(searchProperties.getProductIndexName(), skuList);
        if(successIdList.size() == skuList.size()) {
            productUpdateRetryLogService.complete(msg.getSpuId(), msg.getEsVersion());
        }
    }

    /**
     * 删除商品
     */
    @Transactional
    @Override
    public void delete(ProductUpdateMsg msg) {
        Result<List<EsSkuDto>> remoteResult = productServiceFeign.buildEsSku(msg.getSpuId(), msg.getEsVersion());
        XmallUtils.checkRemoteResult(remoteResult);

        productDeleteRetryLogService.complete(msg.getSpuId());

        if(CollectionUtil.isEmpty(remoteResult.getData())) {
            elasticSearchService.delete(searchProperties.getProductIndexName(), builder ->
                builder.query(q -> q.match(m -> m.field("spuId").query(msg.getSpuId())))
            );
        }
    }

    /**
     * 总数
     */
    @Override
    public Long count(EsSpuSearchRequest request) {
        return elasticSearchService.count(searchProperties.getProductIndexName(), request, builder -> {
            builder.query(q -> q.bool(b -> buildBoolQuery(request)));
        });
    }

    /**
     * 规格参数
     */
    @Override
    public List<EsBaseAttrDto> baseAttrList(EsSpuSearchRequest request) {
        AtomicReference<List<EsBaseAttrDto>> result = new AtomicReference<>();
        result.set(new ArrayList<>());

        elasticSearchService.listPage(searchProperties.getProductIndexName(), request, builder -> {
            builder.query(q -> q.bool(b -> buildBoolQuery(request)));
            buildAggregations(request, builder);

            builder.size(0);
        }, searchResponse -> result.set(buildBaseAttrList(searchResponse)), EsSkuDto.class);

        return result.get();
    }

    /**
     * 分页列表
     */
    @Override
    public Page<EsSkuDto> listPage(EsSpuSearchRequest request) {
        Page<EsSkuDto> result = new Page<>(request.getPage(), request.getPageSize());

        elasticSearchService.listPage(searchProperties.getProductIndexName(), request, builder -> {
            PageUtil.setFirstPageNo(1);
            builder.from(PageUtil.getStart(request.getPage(), request.getPageSize())).size(request.getPageSize());

            builder.query(q -> q.bool(b -> buildBoolQuery(request)));
            buildSort(request, builder);

            buildHighlight(builder);
        }, searchResponse -> {

            result.setTotal(searchResponse.hits().total().value());

            List<Hit<EsSkuDto>> hits = searchResponse.hits().hits();
            result.setRecords(hits.stream().map(hit -> hit.source()).collect(Collectors.toList()));
        }, EsSkuDto.class);

        return result;
    }

    /**
     * 构造bool查询条件
     */
    private BoolQuery.Builder buildBoolQuery(EsSpuSearchRequest request) {
        BoolQuery.Builder boolQueryBuilder = new BoolQuery.Builder();

        // 价格区间
        boolQueryBuilder.filter(s -> s.range(r ->
                r.field("price")
                        .gte(JsonData.of(request.getStartPrice()))
                        .lte(JsonData.of(request.getEndPrice()))
        ));

        // spuId
        if(null != request.getSpuId()) {
            boolQueryBuilder.filter(s -> s.term(m -> m.field("spuId").value(request.getSpuId())));
        }

        // es版本
        if(null != request.getEsVersion()) {
            boolQueryBuilder.filter(s -> s.term(m -> m.field("esVersion").value(request.getEsVersion())));
        }

        // 分类ID
        if(null != request.getCategoryId()) {
            boolQueryBuilder.filter(s -> s.term(m -> m.field("categoryId").value(request.getCategoryId())));
        }

        // 是否有库存
        if(null != request.getHasStock()) {
            boolQueryBuilder.filter(s -> s.term(m -> m.field("hasStock").value(request.getHasStock())));
        }

        // sku标题
        if(StrUtil.isNotBlank(request.getSkuTitle())) {
            boolQueryBuilder.must(q -> q.match(m -> m.field("title").query(request.getSkuTitle())));
        }

        // 规格参数
        request.getAttrs().forEach(attr -> {
            List<FieldValue> attrValues = attr.getAttrValue().stream().map(value -> FieldValue.of(value)).collect(Collectors.toList());

            boolQueryBuilder.filter(q -> q.nested(nq -> nq.path("spuAttrList").query(s -> s.bool(b -> b
                    .must(m -> m.term(t -> t.field("spuAttrList.attrId").value(attr.getAttrId())))
                    .must(m -> m.terms(t -> t.field("spuAttrList.attrValue").terms(v -> v.value(attrValues))))
            ))));
        });

        return boolQueryBuilder;
    }

    /**
     * 构造排序条件
     */
    private void buildSort(EsSpuSearchRequest request, SearchRequest.Builder builder) {
        request.getSorts().forEach(sort -> {
            SortOrder sortOrder = SortEnum.ASC.getName().equals(sort.getSort()) ? SortOrder.Asc : SortOrder.Desc;
            builder.sort(s -> s.field(f -> f.field(sort.getField()).order(sortOrder)));
        });
    }

    /**
     * 构造高亮字段
     */
    private void buildHighlight(SearchRequest.Builder builder) {
        builder.highlight(h -> h.fields("title", f -> f.preTags("<em>").postTags("</em>")));
    }

    /**
     * 构造聚合条件
     */
    private void buildAggregations(EsSpuSearchRequest request, SearchRequest.Builder builder) {
        if(null != request.getCategoryId()) {
            builder.aggregations("spuAttrAgg", spuAttrAgg -> spuAttrAgg
                    .nested(ns -> ns.path("spuAttrList"))
                    .aggregations("attrIdAgg", attrIdAgg -> attrIdAgg
                            .terms(t -> t.field("spuAttrList.attrId").size(10))
                            .aggregations("attrValueAgg", attrValueAgg -> attrValueAgg
                                    .terms(t -> t.field("spuAttrList.attrValue").size(20))
                            )
                    )
            );
        }
    }

    /**
     * 构造规格参数列表
     */
    private List<EsBaseAttrDto> buildBaseAttrList(SearchResponse searchResponse) {
        // 聚合数据
        Map<Long, List<String>> attrIdMap = new HashMap<>();

        Map<String, Aggregate> aggregations = searchResponse.aggregations();
        if(null == aggregations.get("spuAttrAgg")) {
            return new ArrayList<>();
        }

        Map<String, Aggregate> spuAttrAgg = aggregations.get("spuAttrAgg").nested().aggregations();
        if(null == spuAttrAgg.get("attrIdAgg")) {
            return new ArrayList<>();
        }

        spuAttrAgg.get("attrIdAgg").lterms().buckets().array().forEach(attrIdBucket -> {
            Long attrId = attrIdBucket.key();

            Map<String, Aggregate> attrIdAggregations = attrIdBucket.aggregations();
            if(null == attrIdAggregations.get("attrValueAgg")) {
                return;
            }

            Aggregate attrValueAgg = attrIdAggregations.get("attrValueAgg");
            List<String> attrValueList = attrValueAgg.sterms().buckets().array()
                    .stream()
                    .map(attrValueBucket -> attrValueBucket.key().stringValue())
                    .collect(Collectors.toList());

            attrIdMap.put(attrId, attrValueList);
        });

        // 设置属性名称
        if(CollectionUtil.isNotEmpty(attrIdMap)) {
            Result<List<EsBaseAttrDto>> attrInfoResult = productServiceFeign.buildEsBaseAttr(new ArrayList<>(attrIdMap.keySet()));
            XmallUtils.checkRemoteResult(attrInfoResult);

            List<EsBaseAttrDto> attrs = attrInfoResult.getData();
            attrs.forEach(attr -> {
                if(DataTypeEnum.BOOL.getType() == attr.getDataType()) {
                    attr.setValues(Arrays.asList(new EsBaseAttrDto.Value("是", false), new EsBaseAttrDto.Value("否", false)));
                }else {
                    attr.setValues(attrIdMap.get(attr.getId()).stream().map(val -> new EsBaseAttrDto.Value(val, false)).collect(Collectors.toList()));
                }
            });

            return attrs;
        }

        return new ArrayList<>();
    }
}
